fix(asr/nemotron): native-Swift mel front-end to fix iPadOS cold-start zero output (#739)#744
Conversation
…t zero output (#739) Replace the CoreML preprocessor in both Nemotron streaming managers (English + multilingual) with a native-Swift log-mel front-end (NemotronMelExtractor). The CoreML preprocessor's flexible RangeDim audio input made CoreML build the ANE default_function against a 1-sample lower bound, raising "ios17.slice_by_index: zero shape error" and skipping the ANE `main` entry point -- the warning behind the iPadOS cold-start empty-transcript failure in #739. NemotronMelExtractor reproduces NeMo's AudioToMelSpectrogramPreprocessor exactly (n_fft=512, win=400, hop=160, 128 mels, symmetric Hann, preemph=0.97, normalize: NA -- raw log-mel). Parity vs NeMo PyTorch: max |delta| ~= 9e-3. The multilingual manager uses two extractor instances so the on-actor path and the concurrent triple-stage prefetch task never share the extractor's non-thread-safe FFT scratch buffers; the CoreML preprocessor is dropped from SharedNemotronMultilingualModels (multi-stream shares MLModel handles, each manager builds its own extractors). Add a load-time encoder health probe that throws ASRError.encoderInstantiationFailed if the encoder returns all-zero output, so the iPad failure fails loudly instead of emitting empty transcripts. Benchmarks (full sets, native-Swift mel): - English LibriSpeech test-clean (2620 files): 560ms 2.71% / 1120ms 2.58% / 2240ms 2.64% WER - Multilingual FLEURS (7 langs, full splits): AVG 8.62 / 8.33 / 8.30% at 560/1120/2240ms Docs: add Sources/.../Streaming/Nemotron/benchmark.md (English); refresh Documentation/ASR/NemotronMultilingual.md to current tiers (560/1120/2240).
…md; fix swift-format - Move Sources/.../Streaming/Nemotron/benchmark.md content into Documentation/Benchmarks.md as a "Nemotron Speech Streaming 0.6B (English)" section (mirrors the Parakeet Unified section); delete the Sources file. - Reformat KMeansClustering.swift `??` wrapping to satisfy the CI swift-format (Swift 6.1.3) check — pre-existing main drift surfaced because the format job reformats the whole tree.
Sortformer High-Latency Benchmark ResultsES2004a Performance (30.4s latency config)
Sortformer High-Latency • ES2004a • Runtime: 6m 10s • 2026-06-28T21:19:12.613Z |
Supertonic3 Smoke Test ✅
Runtime: 0m44s Note: CI VMs lack a physical Neural Engine; the ANE-bucketed VectorEstimator falls back to CPU here. This validates download + variant resolution + synthesis, not ANE residency/perf. |
PocketTTS Smoke Test ✅
Runtime: 0m5s Note: PocketTTS uses CoreML MLState (macOS 15) KV cache + Mimi streaming state. CI VM lacks physical GPU — audio quality and performance may differ from Apple Silicon. |
Parakeet EOU Benchmark Results ✅Status: Benchmark passed Performance Metrics
Streaming Metrics
Test runtime: 1m22s • 06/28/2026, 04:58 PM EST RTFx = Real-Time Factor (higher is better) • Processing includes: Model inference, audio preprocessing, state management, and file I/O |
VAD Benchmark ResultsPerformance Comparison
Dataset Details
✅: Average F1-Score above 70% |
ASR Benchmark Results ✅Status: All benchmarks passed Parakeet v3 (multilingual)
Parakeet v2 (English-optimized)
Streaming (v3)
Streaming (v2)
Streaming tests use 5 files with 0.5s chunks to simulate real-time audio streaming 25 files per dataset • Test runtime: 12m0s • 06/28/2026, 05:08 PM EST RTFx = Real-Time Factor (higher is better) • Calculated as: Total audio duration ÷ Total processing time Expected RTFx Performance on Physical M1 Hardware:• M1 Mac: ~28x (clean), ~25x (other) Testing methodology follows HuggingFace Open ASR Leaderboard |
✅ Nemotron Multilingual Benchmark — FLEURSFLEURS
Logs (tail) |
Offline VBx Pipeline ResultsSpeaker Diarization Performance (VBx Batch Mode)Optimal clustering with Hungarian algorithm for maximum accuracy
Offline VBx Pipeline Timing BreakdownTime spent in each stage of batch diarization
Speaker Diarization Research ComparisonOffline VBx achieves competitive accuracy with batch processing
Pipeline Details:
🎯 Offline VBx Test • AMI Corpus ES2004a • 1049.0s meeting audio • 134.4s processing • Test runtime: 2m 20s • 06/28/2026, 05:15 PM EST |
✅ Nemotron Multilingual Benchmark — FLEURSFLEURS
Logs (tail) |
Speaker Diarization Benchmark ResultsSpeaker Diarization PerformanceEvaluating "who spoke when" detection accuracy
Diarization Pipeline Timing BreakdownTime spent in each stage of speaker diarization
Speaker Diarization Research ComparisonResearch baselines typically achieve 18-30% DER on standard datasets
Note: RTFx shown above is from GitHub Actions runner. On Apple Silicon with ANE:
🎯 Speaker Diarization Test • AMI Corpus ES2004a • 1049.0s meeting audio • 35.1s diarization time • Test runtime: 2m 9s • 06/28/2026, 05:17 PM EST |
…ring placeholder ground truth (#754) Closes #752 ## Root cause of the 80.8% DER report The 80.8% DER / 80.9% JER on PR #744's CI run was **not a diarizer regression**. From that run's logs: ``` [ERROR] [FluidAudio.Dataset] Failed to download AMI annotations ← Edinburgh server flake (×2) [WARN] [FluidAudio.AMIParser] 📋 Using simplified placeholder ground truth (causes poor DER performance) [INFO] DER: 80.8% JER: 80.9% Speakers: 4 detected / 4 truth ``` The pipeline itself was healthy (4/4 speakers, normal stage timings, normal speaker mapping). The AMI annotation download from `groups.inf.ed.ac.uk` transiently failed, `AMIParser` silently substituted a synthetic placeholder reference (8 evenly-sliced round-robin segments), and the benchmark scored real diarization output against fake ground truth — then posted the result as if genuine. Supporting evidence it was a one-off harness artifact: - The offline VBx job on the **same PR** scored 10.4% (its annotation download, minutes earlier, succeeded) - PR #728's benchmark re-ran 21 minutes later: 15.1% - PR #744's only diarizer-touching diff was a formatting-only change to `KMeansClustering.swift` - Every subsequent run on main-based branches is back at exactly 15.1% ## Changes - **AMIParser**: remove the placeholder ground-truth fallbacks. Search-based loaders now throw `AMIParserError.annotationsNotFound` (or propagate parse errors) instead of returning `generateSimplifiedGroundTruth` output — benchmarks can no longer silently score against fake data. `generateSimplifiedGroundTruth` is deleted; the loaders become synchronous throwing functions. - **DiarizationBenchmark**: `exit(1)` when no meeting produced a result, so CI goes red instead of exiting 0 with empty metrics. - **DatasetDownloader**: retry the annotation zip download (3 attempts with backoff). - **Workflows** (`diarizer-benchmark`, `offline-pipeline`, `sortformer-benchmark`): cache `Datasets/ami_public_1.6.2` so the flaky Edinburgh server is only hit on cache miss; only post the PR benchmark comment when metric extraction succeeded (previously `if: always()`, which would post garbage numbers on failure). - **Tests**: new `AMIParserTests` cases covering the throwing behavior via injectable search roots. ## Verification - **Without annotations**: benchmark logs `annotationsNotFound(subdirectory: "segments")`, writes no results file, exits **1** - **With annotations**: DER **15.1%** / JER 24.8% on ES2004a — exactly the healthy CI baseline ## Also: Documentation/Benchmarks.md re-run (second commit) All diarization tables re-measured 2026-07-03 on an Apple M5 Pro against the official AMI-SDM 16-meeting split: - **Offline**: 10.6% avg DER with `--threshold 0.7`, bit-identical across consecutive runs, reproducing the existing table exactly. Documented that #616 changed the effective CLI default clustering threshold from 0.7 to 0.6 (community-1 preset), which undercounts speakers on 4 meetings (e.g. EN2002a 2/4 → 41.9% DER) and yields 15.5% avg on this split — the documented command now pins `--threshold 0.7`. - **Streaming**: all four configs re-run on the official 16-meeting split (previous tables used an easier 7-meeting subset): 10s/0s 38.2%, 5s/0s 39.0%, 3s/1s 53.3%, 5s/2s 55.9% avg DER. No-overlap configs beat overlap configs; "best configuration" label moved to 10s/0s. - Noted a latent issue (not fixed here): running many overlapping-chunk meetings in one process crashes with `E5RT: Failed to allocate memory IOSurface object` (SIGABRT in MLE5Engine) — the overlap tables were measured one meeting per process.
Summary
Fixes #739 —
StreamingNemotronAsrManagerproduces zero output on iPadOS cold start (empty transcript, works on macOS).Root cause (verified, and it differs from the issue's stated theory): the
ios17.slice_by_index: zero shape error/ "Skipped adding default_function to entry point: main" warning comes from the CoreMLpreprocessormodel's flexibleRangeDimaudio input, not the encoder. CoreML builds the ANEdefault_functionagainst the range's 1-sample lower bound → mel extraction slices to zero frames → the preprocessor's ANEmainentry point is never built. (Confirmed by loading each model onCPU_AND_NE: only the preprocessor warns; the encoder graph is clean. The issue's proposed encodercache_lenre-trace was a misdiagnosis —cache_lenonly feeds boolean masks, not a data-dependent slice.)Fix: remove the CoreML preprocessor from both Nemotron streaming managers and compute log-mel natively in Swift (
NemotronMelExtractor). No model re-conversion / HF re-upload needed.Changes
NemotronMelExtractor— wraps the sharedAudioMelSpectrogram, reproducing NeMo'sAudioToMelSpectrogramPreprocessorexactly (n_fft=512,win=400,hop=160, 128 mels, symmetric Hann,preemph=0.97,normalize: NA→ raw log-mel, no per-feature normalization — the one difference fromUnifiedMelExtractor).StreamingNemotronAsrManager++Pipeline): swappreprocessorMLModel for the extractor.StreamingNemotronMultilingualAsrManager++Pipeline/+Buffers/+Shared): same swap, with two extractor instances (on-actor + concurrent triple-stage prefetch) so the extractor's non-thread-safe FFT buffers are never shared across the prefetch boundary;preprocessordropped fromSharedNemotronMultilingualModels.ASRError.encoderInstantiationFailedif the encoder returns all-zero output (instead of silently emitting empty transcripts).Verification
[56,0]numbers on all published languages.swift buildpasses; new unit tests for the extractor (shape/determinism/not-normalized) and the new error case. (Note:swift testneeds full Xcode for XCTest — runs in CI; built/validated locally + via CLI benchmarks.)Docs
Documentation/Benchmarks.md(LibriSpeech test-clean WER/RTFx per chunk tier).Documentation/ASR/NemotronMultilingual.mdto current tiers (560/1120/2240; dropped obsolete 320ms) with freshly measured numbers.Not covered
ANEProgramProcessRequestDirect status=0x12confirmation requires a physical iPad — not verifiable from a Mac. The mel-parity root-cause case (removing the RangeDim preprocessor) is solid; the probe guarantees the failure can no longer be silent.